package com.gjzhao.fmmall.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.gjzhao.fmmall.dao.CategoryMapper;
import com.gjzhao.fmmall.entity.Category;
import com.gjzhao.fmmall.service.CategoryService;
import com.gjzhao.fmmall.vo.CategoryVO;
import com.gjzhao.fmmall.vo.RespResultVO;
import com.gjzhao.fmmall.vo.ResponseResultVO;
import com.gjzhao.fmmall.vo.RespCode;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

/**
 * @Description: 商品分类 ServiceImpl
 * @Author: gjzhao
 * @Date Created in 2022-01-02 9:40 上午
 */
@Service
public class CategoryServiceImpl implements CategoryService {

    private static final Logger logger = LoggerFactory.getLogger(CategoryServiceImpl.class);

    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 查看商品详情分类
     *
     * @return
     */
    @Override
    public ResponseResultVO listCategories() {
        List<CategoryVO> categories = categoryMapper.selectAllCategories();
        ResponseResultVO resultVO = new ResponseResultVO(RespCode.SUCCESS.getCode(), "success", categories);
        return resultVO;
    }

    /**
     * 商品分类-三级分类列表详情(使用redis方式)
     *
     * @return
     */
    @Override
    public RespResultVO listCategoriesByRedis() {

        try {

            //1.查询缓存
            String categoriesStr = stringRedisTemplate.opsForValue().get("categories");

            if (StrUtil.isNotBlank(categoriesStr)) {
                // 1.1 如果缓存中存在分类信息
                JSONArray jsonArray = JSONUtil.parseArray(categoriesStr);
                List<CategoryVO> categoryVOList = JSONUtil.toList(jsonArray, CategoryVO.class);

                logger.info("-----从redis中查询到分类信息:{}", categoryVOList);
                return RespResultVO.success(categoryVOList);
            } else {
                // 1.2  redis中不存在分类信息，需要先从数据库中进行查询
                logger.info("-----从redis中未查询到分类信息，需要从数据库中查询");
                List<CategoryVO> categoriesList = categoryMapper.selectAllCategories();

                if (CollectionUtil.isNotEmpty(categoriesList)) {
                    // 1.2.1 能够从数据库中查询到数据，缓存到redis，同时设置过期时间
                    categoriesStr = JSONUtil.toJsonStr(categoriesList);
                    stringRedisTemplate.boundValueOps("categories").set(categoriesStr, 7, TimeUnit.DAYS);

                    logger.info("-----从数据库中查询到分类信息，已缓存到redis，过期时间为7天。:{}", categoriesStr);
                    return RespResultVO.success(categoriesList);
                } else {
                    // 1.2.2 从数据库中未查询到数据，则返回为空
                    logger.info("-----从数据库中未查询到分类信息");
                    return RespResultVO.failed(RespCode.FAILED, "当前未查询到分类信息");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return RespResultVO.failed(RespCode.FAILED, e.getMessage());
        }
    }


    /**
     * 商品分类-三级分类列表详情(使用redis方式，高并发场景)
     *
     * @return
     */
    @Override
    public RespResultVO listCategoriesByRedisByAndConcurrent1() {

        try {
            //1.查询缓存
            String categoriesStr = stringRedisTemplate.opsForValue().get("categories");

            if (StrUtil.isNotBlank(categoriesStr)) {
                // 1.1 如果缓存中存在分类信息
                JSONArray jsonArray = JSONUtil.parseArray(categoriesStr);
                List<CategoryVO> categoryVOList = JSONUtil.toList(jsonArray, CategoryVO.class);

                logger.info("-----从redis中查询到分类信息:{}----", categoryVOList);
                return RespResultVO.success(categoryVOList);
            } else {

                logger.info("-----从redis中未查询到分类信息，需要从数据库中查询-----");

                // 在高并发场景下，可能存在大量请求查询redis时不存在数据，使用jvm层面的锁，让其串行查询
                // this指当前 CategoryServiceImpl 实例对象，此对象在 Spring 中只有一个，保证多个线程使用同一把锁
                synchronized (this) {

                    logger.info("-------为避免大量请求同时打到数据库，再一次查询redis------");

                    // 1.2 再一次从redis中进行查询(可能其他线程有从数据中查询到数据，放到redis中)
                    categoriesStr = stringRedisTemplate.opsForValue().get("categories");

                    if (StrUtil.isNotBlank(categoriesStr)) {
                        // 1.2.1 如果能够查询到，则直接返回
                        JSONArray jsonArray = JSONUtil.parseArray(categoriesStr);
                        List<CategoryVO> categoryVOList = JSONUtil.toList(jsonArray, CategoryVO.class);

                        logger.info("-------从redis查询到数据，返回成功------");

                        return RespResultVO.success(categoryVOList);
                    } else {
                        // 1.2.2 如果不能查询到，再从数据库中进行查询
                        List<CategoryVO> categoriesList = categoryMapper.selectAllCategories();

                        if (CollectionUtil.isNotEmpty(categoriesList)) {
                            //能够从数据库中查询到数据，缓存到redis，同时设置过期时间
                            categoriesStr = JSONUtil.toJsonStr(categoriesList);
                            stringRedisTemplate.boundValueOps("categories").set(categoriesStr, 7, TimeUnit.DAYS);

                            logger.info("-----从数据库中查询到分类信息，已缓存到redis，过期时间为7天。:{}----", categoriesStr);
                            return RespResultVO.success(categoriesList);
                        } else {
                            // 从数据库中未查询到数据，则返回为空
                            logger.info("-----从数据库中未查询到分类信息-----");

                            return RespResultVO.failed(RespCode.FAILED, "当前未查询到分类信息");
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return RespResultVO.failed(RespCode.FAILED, e.getMessage());
        }
    }


    /**
     * 商品分类-三级分类列表详情(使用redis方式，高并发场景下-解决缓存击穿+缓存穿透问题)
     * @return
     */
    @Override
    public RespResultVO listCategoriesByRedisByAndConcurrent2() {
        try {
            //1.查询缓存
            String categoriesStr = stringRedisTemplate.opsForValue().get("categories");

            if (StrUtil.isNotBlank(categoriesStr)) {
                // 1.1 如果缓存中存在分类信息
                JSONArray jsonArray = JSONUtil.parseArray(categoriesStr);
                List<CategoryVO> categoryVOList = JSONUtil.toList(jsonArray, CategoryVO.class);

                logger.info("-----从redis中查询到分类信息:{}----", categoryVOList);
                return RespResultVO.success(categoryVOList);
            } else {

                logger.info("-----从redis中未查询到分类信息，需要从数据库中查询-----");

                // 在高并发场景下，可能存在大量请求查询redis时不存在数据，使用jvm层面的锁，让其串行查询
                // this指当前 CategoryServiceImpl 实例对象，此对象在 Spring 中只有一个，保证多个线程使用同一把锁
                synchronized (this) {

                    logger.info("-------为避免大量请求同时打到数据库，再一次查询redis------");

                    // 1.2 再一次从redis中进行查询(可能其他线程有从数据中查询到数据，放到redis中)
                    categoriesStr = stringRedisTemplate.opsForValue().get("categories");

                    if (StrUtil.isNotBlank(categoriesStr)) {
                        // 1.2.1 如果能够查询到，则直接返回
                        JSONArray jsonArray = JSONUtil.parseArray(categoriesStr);
                        List<CategoryVO> categoryVOList = JSONUtil.toList(jsonArray, CategoryVO.class);

                        logger.info("-------从redis查询到数据，返回成功------");

                        return RespResultVO.success(categoryVOList);
                    } else {
                        // 1.2.2 如果不能查询到，再从数据库中进行查询
                        List<CategoryVO> categoriesList = categoryMapper.selectAllCategories();
                        categoriesList = null;
                        if (CollectionUtil.isNotEmpty(categoriesList)) {

                            // one 能够从数据库中查询到数据，缓存到redis，同时设置过期时间
                            categoriesStr = JSONUtil.toJsonStr(categoriesList);
                            stringRedisTemplate.boundValueOps("categories").set(categoriesStr, 7, TimeUnit.DAYS);

                            logger.info("-----从数据库中查询到分类信息，已缓存到redis，过期时间为7天。:{}----", categoriesStr);
                            return RespResultVO.success(categoriesList);
                        } else {
                            // two 从数据库中未查询到数据，则返回为空，同时在redis中添加一个非空数据
                            logger.info("-----从数据库中未查询到分类信息-----");

                            logger.info("-----高并发场景下，为避免大量请求查询数据库，在redis中缓存非空数据，同时设置过期时间----");

                            // 往redis中添加一个非空数据 todo 为保证强一致性，往数据库中添加数据时，同时写一份到redis中
                            categoriesList = new ArrayList<>();
                            categoriesStr = JSONUtil.toJsonStr(categoriesList);
                            stringRedisTemplate.boundValueOps("categories").set(categoriesStr, 10, TimeUnit.SECONDS);

                            return RespResultVO.failed(RespCode.FAILED, "当前未查询到分类信息");
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return RespResultVO.failed(RespCode.FAILED, e.getMessage());
        }
    }

    /**
     * 查看商品一级分类
     *
     * @return
     */
    @Override
    public ResponseResultVO listFirstLevelCategories() {
        List<Category> categories = categoryMapper.selectFirstLevelCategories();
        ResponseResultVO resultVO = new ResponseResultVO(RespCode.SUCCESS.getCode(), "success", categories);
        return resultVO;
    }

    /**
     * 查询该分类下的子分类
     *
     * @return
     */
    @Override
    public ResponseResultVO listParentCategories(int parentId) {
        List<CategoryVO> categories = categoryMapper.selectParentCategories(parentId);
        ResponseResultVO resultVO = new ResponseResultVO(RespCode.SUCCESS.getCode(), "success", categories);
        return resultVO;
    }

    /**
     * 查询一级分类下推荐商品列表
     *
     * @return
     */
    @Override
    public ResponseResultVO listFirstLevelCategoryRecommendsProduct() {
        List<CategoryVO> categories = categoryMapper.selectFirstLevelCategoryRecommendProductList();
        ResponseResultVO resultVO = new ResponseResultVO(RespCode.SUCCESS.getCode(), "success", categories);
        return resultVO;
    }
}
